Micron Document
πŸŽ–οΈGitΠ―Ρ€Π°πŸŽ–οΈ

Node / meshtastic / Meshtastic-Android / files / core / service / src / commonTest / kotlin / org / meshtastic / core / service / SharedRadioInterfaceServiceLivenessTest.kt

Displaying Raw β€’ Download

core/service/src/commonTest/kotlin/org/meshtastic/core/service/SharedRadioInterfaceServiceLivenessTest.kt 35fba4d56d1a6fa0a2f44fee9c9a550c476b8c96 (35fba4d5) Text, 25.69 KB

T8b949e/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
Tff7b72package T7ee787org.meshtastic.core.service

Tff7b72import T7ee787androidx.lifecycle.Lifecycle
Tff7b72import T7ee787androidx.lifecycle.LifecycleEventObserver
Tff7b72import T7ee787androidx.lifecycle.LifecycleObserver
Tff7b72import T7ee787androidx.lifecycle.LifecycleOwner
Tff7b72import T7ee787dev.mokkery.MockMode
Tff7b72import T7ee787dev.mokkery.answering.calls
Tff7b72import T7ee787dev.mokkery.answering.returns
Tff7b72import T7ee787dev.mokkery.every
Tff7b72import T7ee787dev.mokkery.matcher.any
Tff7b72import T7ee787dev.mokkery.mock
Tff7b72import T7ee787kotlinx.coroutines.CompletableDeferred
Tff7b72import T7ee787kotlinx.coroutines.Dispatchers
Tff7b72import T7ee787kotlinx.coroutines.ExperimentalCoroutinesApi
Tff7b72import T7ee787kotlinx.coroutines.flow.MutableSharedFlow
Tff7b72import T7ee787kotlinx.coroutines.flow.MutableStateFlow
Tff7b72import T7ee787kotlinx.coroutines.launch
Tff7b72import T7ee787kotlinx.coroutines.test.UnconfinedTestDispatcher
Tff7b72import T7ee787kotlinx.coroutines.test.advanceTimeBy
Tff7b72import T7ee787kotlinx.coroutines.test.resetMain
Tff7b72import T7ee787kotlinx.coroutines.test.runTest
Tff7b72import T7ee787kotlinx.coroutines.test.setMain
Tff7b72import T7ee787org.meshtastic.core.di.CoroutineDispatchers
Tff7b72import T7ee787org.meshtastic.core.model.ConnectionState
Tff7b72import T7ee787org.meshtastic.core.model.DeviceType
Tff7b72import T7ee787org.meshtastic.core.network.repository.NetworkRepository
Tff7b72import T7ee787org.meshtastic.core.repository.PlatformAnalytics
Tff7b72import T7ee787org.meshtastic.core.repository.RadioTransport
Tff7b72import T7ee787org.meshtastic.core.repository.RadioTransportFactory
Tff7b72import T7ee787org.meshtastic.core.testing.FakeBluetoothRepository
Tff7b72import T7ee787org.meshtastic.core.testing.FakeRadioPrefs
Tff7b72import T7ee787org.meshtastic.core.testing.FakeRadioTransport
Tff7b72import T7ee787kotlin.test.AfterTest
Tff7b72import T7ee787kotlin.test.BeforeTest
Tff7b72import T7ee787kotlin.test.Test
Tff7b72import T7ee787kotlin.test.assertEquals
Tff7b72import T7ee787kotlin.test.assertFalse
Tff7b72import T7ee787kotlin.test.assertTrue

T8b949e/**
* Service-level tests for [SharedRadioInterfaceService] liveness detection.
*
* Uses a controllable clock via [SharedRadioInterfaceService.clockMillis] so [onConnect], [handleFromRadio], and
* [checkLiveness] all share one coherent time source β€” no mixing of real wall-clock with test time.
*
* A counting transport factory returns a fresh [FakeRadioTransport] per createTransport() call so we can observe how
* many restarts actually occurred.
*/
Tf0883e@OptInTb4b4b4(Te6edf3ExperimentalCoroutinesApiTff7b72::Te6edf3classTb4b4b4)
Tff7b72class T56d364SharedRadioInterfaceServiceLivenessTest Tb4b4b4{

Tff7b72private Tff7b72val Te6edf3testDispatcher Tff7b72= Te6edf3UnconfinedTestDispatcherTb4b4b4(Tb4b4b4)
Tff7b72private Tff7b72val Te6edf3dispatchers Tff7b72= Te6edf3CoroutineDispatchersTb4b4b4(Te6edf3io Tff7b72= Te6edf3testDispatcherTb4b4b4, Te6edf3main Tff7b72= Te6edf3testDispatcherTb4b4b4, Te6edf3default Tff7b72= Te6edf3testDispatcherTb4b4b4)

Tff7b72private Tff7b72lateinit Tff7b72var Te6edf3processLifecycleOwnerTb4b4b4: Te6edf3TestLifecycleOwner

Tf0883e@BeforeTest
Tff7b72fun Td2a8ffsetUpTb4b4b4(Tb4b4b4) Tb4b4b4{
T8b949e// processLifecycle.coroutineScope uses Dispatchers.Main.immediate internally;
T8b949e// JVM tests must install a Main dispatcher or get IllegalStateException.
Te6edf3DispatchersTb4b4b4.Te6edf3setMainTb4b4b4(Te6edf3testDispatcherTb4b4b4)
T8b949e// Create the lifecycle owner AFTER setMain so Robolectric's main thread is ready.
T8b949e// Field initializers run before @BeforeTest, which is too early for Robolectric.
Te6edf3processLifecycleOwner Tff7b72= Te6edf3TestLifecycleOwnerTb4b4b4(Tb4b4b4)
Tb4b4b4}

Tf0883e@AfterTest
Tff7b72fun Td2a8fftearDownTb4b4b4(Tb4b4b4) Tb4b4b4{
T8b949e// Release any suspended close gate so a held in-flight restart can complete; otherwise
T8b949e// disconnect() below would block forever on the gated transport's close().
T8b949e// NOTE: relies on UnconfinedTestDispatcher resuming the gated close() inline when
T8b949e// complete(Unit) is called β€” if testDispatcher is ever changed to StandardTestDispatcher,
T8b949e// add testDispatcher.scheduler.runCurrent() here before runBlocking to avoid a mutex deadlock.
Te6edf3activeCloseGateTff7b72?.Te6edf3completeTb4b4b4(Tffa657UnitTb4b4b4)
Te6edf3activeCloseGate Tff7b72= Tff7b72null
T8b949e// Service cleanup is handled per-test in try/finally blocks β€” each test calls
T8b949e// service.disconnect() + advanceTimeBy in a finally clause. tearDown cannot use
T8b949e// runBlocking { services.forEach { it.disconnect() } } because it deadlocks on
T8b949e// Robolectric's main thread (androidHostTest target).
Te6edf3servicesTb4b4b4.Te6edf3clearTb4b4b4(Tb4b4b4)
Te6edf3createdTransportsTb4b4b4.Te6edf3clearTb4b4b4(Tb4b4b4)
T8b949e// CRITICAL: Destroy the lifecycle to cancel processLifecycle.coroutineScope and all
T8b949e// leaked collectors (devAddr, bluetoothRepository.state, networkRepository.networkAvailable).
T8b949e// Without this, those infinite flow collectors keep the forked test JVM alive after tests
T8b949e// complete, causing Gradle to hang at subsequent :core:*:allTests tasks.
Te6edf3processLifecycleOwnerTb4b4b4.Te6edf3destroyTb4b4b4(Tb4b4b4)
T8b949e// Let pending cancellations propagate before resetting the Main dispatcher. Use runCurrent
T8b949e// (NOT advanceUntilIdle): the test bodies already disconnect every service, so no heartbeat
T8b949e// loop should be active here β€” advanceUntilIdle would hang if one somehow survived.
Te6edf3testDispatcherTb4b4b4.Te6edf3schedulerTb4b4b4.Te6edf3runCurrentTb4b4b4(Tb4b4b4)
Te6edf3DispatchersTb4b4b4.Te6edf3resetMainTb4b4b4(Tb4b4b4)
Tb4b4b4}

Tff7b72private Tff7b72val Te6edf3bluetoothRepository Tff7b72= Te6edf3FakeBluetoothRepositoryTb4b4b4(Tb4b4b4)
Tff7b72private Tff7b72val Te6edf3radioPrefs Tff7b72= Te6edf3FakeRadioPrefsTb4b4b4(Tb4b4b4)

Tff7b72private Tff7b72val Te6edf3networkRepositoryTb4b4b4: Te6edf3NetworkRepository Tff7b72= Te6edf3mockTb4b4b4(Te6edf3MockModeTb4b4b4.Te6edf3autofillTb4b4b4)
Tff7b72private Tff7b72val Te6edf3analyticsTb4b4b4: Te6edf3PlatformAnalytics Tff7b72= Te6edf3mockTb4b4b4(Te6edf3MockModeTb4b4b4.Te6edf3autofillTb4b4b4)

T8b949e/**
* Minimal [LifecycleOwner] for tests that avoids [LifecycleRegistry], which enforces main-thread checks and throws
* `RuntimeException` under Robolectric (androidHostTest). This custom [Lifecycle] dispatches ON_DESTROY to
* registered [LifecycleEventObserver]s so `lifecycleScope` cancels correctly.
*/
Tff7b72private Tff7b72class T56d364TestLifecycleOwner Tb4b4b4: Te6edf3LifecycleOwner Tb4b4b4{
Tff7b72private Tff7b72val Te6edf3observers Tff7b72= Te6edf3mutableListOfTff7b72<Te6edf3LifecycleObserverTff7b72>Tb4b4b4(Tb4b4b4)
Tff7b72private Tff7b72var Te6edf3state Tff7b72= Te6edf3LifecycleTb4b4b4.Te6edf3StateTb4b4b4.Te6edf3RESUMED

Tff7b72override Tff7b72val Te6edf3lifecycleTb4b4b4: Te6edf3Lifecycle Tff7b72=
Tff7b72object Tb4b4b4: T56d364LifecycleTb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72override Tff7b72fun Td2a8ffaddObserverTb4b4b4(Te6edf3observerTb4b4b4: Te6edf3LifecycleObserverTb4b4b4) Tb4b4b4{
Te6edf3observersTb4b4b4.Te6edf3addTb4b4b4(Te6edf3observerTb4b4b4)
Tb4b4b4}

Tff7b72override Tff7b72fun Td2a8ffremoveObserverTb4b4b4(Te6edf3observerTb4b4b4: Te6edf3LifecycleObserverTb4b4b4) Tb4b4b4{
Te6edf3observersTb4b4b4.Te6edf3removeTb4b4b4(Te6edf3observerTb4b4b4)
Tb4b4b4}

Tff7b72override Tff7b72val Te6edf3currentStateTb4b4b4: Te6edf3LifecycleTb4b4b4.Te6edf3State
Tff7b72getTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3state
Tb4b4b4}

Tff7b72fun Td2a8ffdestroyTb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3state Tff7b72= Te6edf3LifecycleTb4b4b4.Te6edf3StateTb4b4b4.Te6edf3DESTROYED
Tff7b72val Te6edf3event Tff7b72= Te6edf3LifecycleTb4b4b4.Te6edf3EventTb4b4b4.Te6edf3ON_DESTROY
Te6edf3observersTb4b4b4.Te6edf3toListTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3forEach Tb4b4b4{ Te6edf3observer Tff7b72-Tff7b72>
Tb4b4b4(Te6edf3observer Tff7b72as? Te6edf3LifecycleEventObserverTb4b4b4)Tff7b72?.Te6edf3onStateChangedTb4b4b4(Tff7b72thisTf0883e@TestLifecycleOwnerTb4b4b4, Te6edf3eventTb4b4b4)
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}

T8b949e/**
* Test-only [RadioTransport] whose [close] suspends on a [CompletableDeferred] gate.
*
* The liveness restart path calls `stopTransportLocked` β†’ `currentTransport.close()` inside a launched coroutine.
* With the default [FakeRadioTransport], `close()` returns without suspending, so under [UnconfinedTestDispatcher]
* the entire restart completes synchronously during `checkLiveness()` and a second `checkLiveness()` never observes
* an in-flight restart. By awaiting a gate inside `close()`, this fake holds the restart genuinely suspended
* mid-flight, letting a test deterministically exercise the in-flight overlap window and prove the second check
* does not stack another restart/close.
*
* The gate is shared across instances; once completed by the test, any pending or subsequent `close()` resumes
* immediately.
*/
Tff7b72private Tff7b72class T56d364GatedFakeRadioTransportTb4b4b4(Tff7b72private Tff7b72val Te6edf3closeGateTb4b4b4: Te6edf3CompletableDeferredTff7b72<Tffa657UnitTff7b72>Tb4b4b4) Tb4b4b4: Te6edf3RadioTransport Tb4b4b4{
Tff7b72var Te6edf3closeCalled Tff7b72= Tff7b72false
Te6edf3private Tff7b72set

Tff7b72var Te6edf3closeCount Tff7b72= T79c0ff0
Te6edf3private Tff7b72set

Tff7b72var Te6edf3closeCompletedCount Tff7b72= T79c0ff0
Te6edf3private Tff7b72set

T8b949e// Liveness restart skips the polite-disconnect frame (sendPoliteDisconnect = false), so no
T8b949e// outbound data is expected; satisfy the contract with a no-op.
Tff7b72override Tff7b72fun Td2a8ffhandleSendToRadioTb4b4b4(Te6edf3pTb4b4b4: Te6edf3ByteArrayTb4b4b4) Tff7b72= Tffa657Unit

Tff7b72override Tff7b72suspend Tff7b72fun Td2a8ffcloseTb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3closeCalled Tff7b72= Tff7b72true
Te6edf3closeCountTff7b72+Tff7b72+
T8b949e// Suspend here until the test releases the gate, holding the restart in-flight.
Te6edf3closeGateTb4b4b4.Te6edf3awaitTb4b4b4(Tb4b4b4)
Te6edf3closeCompletedCountTff7b72+Tff7b72+
Tb4b4b4}
Tb4b4b4}

T8b949e/** Controllable clock β€” tests advance this manually so all time comparisons are deterministic. */
Tff7b72private Tff7b72var Te6edf3clockTb4b4b4: Tffa657Long Tff7b72= T79c0ff0L

T8b949e/**
* Tracks every [SharedRadioInterfaceService] created via [createConnectedService] so [tearDown] can disconnect them
* deterministically. Destroying the process lifecycle does NOT cancel the service's private `_serviceScope` (which
* hosts the heartbeat loop), so we must call `disconnect()` explicitly.
*/
Tff7b72private Tff7b72val Te6edf3services Tff7b72= Te6edf3mutableListOfTff7b72<Te6edf3SharedRadioInterfaceServiceTff7b72>Tb4b4b4(Tb4b4b4)

T8b949e/**
* Tracks the suspended close gate for the in-flight restart test so [tearDown] can release it even if the test body
* throws. Without this, a failed assertion before `closeGate.complete(Unit)` would leave a restart suspended
* forever and hang teardown.
*/
Tff7b72private Tff7b72var Te6edf3activeCloseGateTb4b4b4: Te6edf3CompletableDeferredTff7b72<Tffa657UnitTff7b72>Tff7b72? Tff7b72= Tff7b72null

T8b949e/** Tracks all transports created by the factory so we can count restarts and inspect sent data. */
Tff7b72private Tff7b72val Te6edf3createdTransports Tff7b72= Te6edf3mutableListOfTff7b72<Te6edf3FakeRadioTransportTff7b72>Tb4b4b4(Tb4b4b4)
Tff7b72private Tff7b72val Te6edf3transportFactoryTb4b4b4: Te6edf3RadioTransportFactory Tff7b72= Te6edf3mockTb4b4b4(Te6edf3MockModeTb4b4b4.Te6edf3autofillTb4b4b4)

T8b949e/**
* Creates a [SharedRadioInterfaceService] with a controllable clock and a factory that returns a fresh
* [FakeRadioTransport] per createTransport() call. After construction, calls [connect] then explicitly [onConnect]
* to bring the service to Connected state (FakeRadioTransport does not call onConnect itself).
*
* Pass [transportProvider] to swap in a custom test double (e.g. a suspending-close fake) instead of the default
* [FakeRadioTransport]; the default records each created transport in [createdTransports].
*/
Tff7b72private Tff7b72fun Td2a8ffcreateConnectedServiceTb4b4b4(
Te6edf3addressTb4b4b4: Tffa657StringTb4b4b4,
Te6edf3transportProviderTb4b4b4: Tb4b4b4(Tb4b4b4) Tff7b72-Tff7b72> Te6edf3RadioTransport Tff7b72= Tb4b4b4{ Te6edf3FakeRadioTransportTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3also Tb4b4b4{ Te6edf3createdTransportsTb4b4b4.Te6edf3addTb4b4b4(Tffa657itTb4b4b4) Tb4b4b4} Tb4b4b4}Tb4b4b4,
Tb4b4b4)Tb4b4b4: Te6edf3SharedRadioInterfaceService Tb4b4b4{
Te6edf3every Tb4b4b4{ Te6edf3networkRepositoryTb4b4b4.Te6edf3networkAvailable Tb4b4b4} Te6edf3returns Te6edf3MutableStateFlowTb4b4b4(Tff7b72trueTb4b4b4)
Te6edf3every Tb4b4b4{ Te6edf3networkRepositoryTb4b4b4.Te6edf3resolvedList Tb4b4b4} Te6edf3returns Te6edf3MutableSharedFlowTb4b4b4(Tb4b4b4)
Te6edf3every Tb4b4b4{ Te6edf3analyticsTb4b4b4.Te6edf3isPlatformServicesAvailable Tb4b4b4} Te6edf3returns Tff7b72false
Te6edf3every Tb4b4b4{ Te6edf3transportFactoryTb4b4b4.Te6edf3supportedDeviceTypes Tb4b4b4} Te6edf3returns Te6edf3listOfTb4b4b4(Te6edf3DeviceTypeTb4b4b4.Te6edf3BLETb4b4b4)
Te6edf3every Tb4b4b4{ Te6edf3transportFactoryTb4b4b4.Te6edf3isMockTransportTb4b4b4(Tb4b4b4) Tb4b4b4} Te6edf3returns Tff7b72false
Te6edf3every Tb4b4b4{ Te6edf3transportFactoryTb4b4b4.Te6edf3isAddressValidTb4b4b4(Te6edf3anyTb4b4b4(Tb4b4b4)Tb4b4b4) Tb4b4b4} Te6edf3returns Tff7b72true
Te6edf3every Tb4b4b4{ Te6edf3transportFactoryTb4b4b4.Te6edf3toInterfaceAddressTb4b4b4(Te6edf3anyTb4b4b4(Tb4b4b4)Tb4b4b4, Te6edf3anyTb4b4b4(Tb4b4b4)Tb4b4b4) Tb4b4b4} Te6edf3returns Te6edf3address
Te6edf3every Tb4b4b4{ Te6edf3transportFactoryTb4b4b4.Te6edf3createTransportTb4b4b4(Te6edf3anyTb4b4b4(Tb4b4b4)Tb4b4b4, Te6edf3anyTb4b4b4(Tb4b4b4)Tb4b4b4) Tb4b4b4} Te6edf3calls Tb4b4b4{ Te6edf3transportProviderTb4b4b4(Tb4b4b4) Tb4b4b4}

Te6edf3radioPrefsTb4b4b4.Te6edf3setDevAddrTb4b4b4(Te6edf3addressTb4b4b4)

Tff7b72val Te6edf3service Tff7b72=
Te6edf3SharedRadioInterfaceServiceTb4b4b4(
Te6edf3dispatchers Tff7b72= Te6edf3dispatchersTb4b4b4,
Te6edf3bluetoothRepository Tff7b72= Te6edf3bluetoothRepositoryTb4b4b4,
Te6edf3networkRepository Tff7b72= Te6edf3networkRepositoryTb4b4b4,
Te6edf3processLifecycle Tff7b72= Te6edf3processLifecycleOwnerTb4b4b4.Te6edf3lifecycleTb4b4b4,
Te6edf3radioPrefs Tff7b72= Te6edf3radioPrefsTb4b4b4,
Te6edf3transportFactory Tff7b72= Te6edf3transportFactoryTb4b4b4,
Te6edf3analytics Tff7b72= Te6edf3analyticsTb4b4b4,
Tb4b4b4)
Te6edf3serviceTb4b4b4.Te6edf3clockMillis Tff7b72= Tb4b4b4{ Te6edf3clock Tb4b4b4}
T8b949e// Register the service so tearDown can disconnect it deterministically (the heartbeat loop
T8b949e// launched in _serviceScope would otherwise outlive the test).
Te6edf3servicesTb4b4b4.Te6edf3addTb4b4b4(Te6edf3serviceTb4b4b4)
Te6edf3serviceTb4b4b4.Te6edf3connectTb4b4b4(Tb4b4b4)
Te6edf3serviceTb4b4b4.Te6edf3onConnectTb4b4b4(Tb4b4b4)
Tff7b72return Te6edf3service
Tb4b4b4}

T8b949e// ─── BLE: Liveness timeout triggers recovery ───────────────────────────────────────────────

Tf0883e@Test
Tff7b72fun Td2a8ff`BLE liveness timeout closes old transport and creates fresh one`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTestTb4b4b4(Te6edf3testDispatcherTb4b4b4) Tb4b4b4{
Te6edf3clock Tff7b72= T79c0ff0L
Tff7b72val Te6edf3service Tff7b72= Te6edf3createConnectedServiceTb4b4b4(Ta5d6ff"Ta5d6ffxAA:BB:CC:DD:EE:FFTa5d6ff"Tb4b4b4)
Tff7b72try Tb4b4b4{
Te6edf3assertEqualsTb4b4b4(T79c0ff1Tb4b4b4, Te6edf3createdTransportsTb4b4b4.Te6edf3sizeTb4b4b4, Ta5d6ff"Ta5d6ffInitial connect should create one transportTa5d6ff"Tb4b4b4)

Te6edf3clock Tff7b72= T79c0ff6T79c0ff5Te6edf3_000L
Te6edf3serviceTb4b4b4.Te6edf3checkLivenessTb4b4b4(Tb4b4b4)
T8b949e// Under UnconfinedTestDispatcher the liveness restart (sendPoliteDisconnect = false) runs
T8b949e// inline during checkLiveness(). runCurrent/advanceTimeBy are belt-and-suspenders; the
T8b949e// real 500ms polite-disconnect delay is covered by the trailing service.disconnect() below.
Te6edf3testDispatcherTb4b4b4.Te6edf3schedulerTb4b4b4.Te6edf3runCurrentTb4b4b4(Tb4b4b4)
Te6edf3advanceTimeByTb4b4b4(T79c0ff1Te6edf3_000LTb4b4b4)

Te6edf3assertEqualsTb4b4b4(T79c0ff2Tb4b4b4, Te6edf3createdTransportsTb4b4b4.Te6edf3sizeTb4b4b4, Ta5d6ff"Ta5d6ffLiveness restart should create exactly one fresh transportTa5d6ff"Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3createdTransportsTb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3closeCalledTb4b4b4, Ta5d6ff"Ta5d6ffOld transport must be closedTa5d6ff"Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff1Tb4b4b4, Te6edf3createdTransportsTb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3closeCountTb4b4b4, Ta5d6ff"Ta5d6ffOld transport closed exactly onceTa5d6ff"Tb4b4b4)
Tb4b4b4} Tff7b72finally Tb4b4b4{
Te6edf3serviceTb4b4b4.Te6edf3disconnectTb4b4b4(Tb4b4b4)
Te6edf3advanceTimeByTb4b4b4(T79c0ff1Te6edf3_000LTb4b4b4)
Tb4b4b4}
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`BLE liveness restart does not emit permanent Disconnected`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTestTb4b4b4(Te6edf3testDispatcherTb4b4b4) Tb4b4b4{
Te6edf3clock Tff7b72= T79c0ff0L
Tff7b72val Te6edf3service Tff7b72= Te6edf3createConnectedServiceTb4b4b4(Ta5d6ff"Ta5d6ffxAA:BB:CC:DD:EE:FFTa5d6ff"Tb4b4b4)

Tff7b72try Tb4b4b4{
T8b949e// Capture all state transitions during the liveness recovery
Tff7b72val Te6edf3stateEmissions Tff7b72= Te6edf3mutableListOfTff7b72<Te6edf3ConnectionStateTff7b72>Tb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3collectJob Tff7b72= Te6edf3backgroundScopeTb4b4b4.Te6edf3launch Tb4b4b4{ Te6edf3serviceTb4b4b4.Te6edf3connectionStateTb4b4b4.Te6edf3collect Tb4b4b4{ Te6edf3stateEmissionsTb4b4b4.Te6edf3addTb4b4b4(Tffa657itTb4b4b4) Tb4b4b4} Tb4b4b4}

Te6edf3clock Tff7b72= T79c0ff6T79c0ff5Te6edf3_000L
Te6edf3serviceTb4b4b4.Te6edf3checkLivenessTb4b4b4(Tb4b4b4)
T8b949e// The restart completes inline under UnconfinedTestDispatcher; runCurrent/advanceTimeBy
T8b949e// are belt-and-suspenders. The trailing disconnect() covers its own 500ms polite delay.
Te6edf3testDispatcherTb4b4b4.Te6edf3schedulerTb4b4b4.Te6edf3runCurrentTb4b4b4(Tb4b4b4)
Te6edf3advanceTimeByTb4b4b4(T79c0ff1Te6edf3_000LTb4b4b4)

Te6edf3collectJobTb4b4b4.Te6edf3cancelTb4b4b4(Tb4b4b4)

T8b949e// Recovery must NEVER emit permanent Disconnected
Te6edf3assertFalseTb4b4b4(
Te6edf3ConnectionStateTb4b4b4.Te6edf3Disconnected Tff7b72in Te6edf3stateEmissionsTb4b4b4,
Ta5d6ff"Ta5d6ffAutomatic recovery must not emit permanent Disconnected state Ta5d6ff" Tff7b72+ Ta5d6ff"Ta5d6ff(emitted: Tffd700$Te6edf3stateEmissionsTa5d6ff)Ta5d6ff"Tb4b4b4,
Tb4b4b4)
Tb4b4b4} Tff7b72finally Tb4b4b4{
Te6edf3serviceTb4b4b4.Te6edf3disconnectTb4b4b4(Tb4b4b4)
Te6edf3advanceTimeByTb4b4b4(T79c0ff1Te6edf3_000LTb4b4b4)
Tb4b4b4}
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`BLE liveness restart does not emit user-facing connection error`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTestTb4b4b4(Te6edf3testDispatcherTb4b4b4) Tb4b4b4{
Te6edf3clock Tff7b72= T79c0ff0L
Tff7b72val Te6edf3service Tff7b72= Te6edf3createConnectedServiceTb4b4b4(Ta5d6ff"Ta5d6ffxAA:BB:CC:DD:EE:FFTa5d6ff"Tb4b4b4)

Tff7b72try Tb4b4b4{
T8b949e// Collect connectionError emissions β€” automatic liveness recovery must be silent.
T8b949e// _connectionError is a no-replay SharedFlow, so the collector must subscribe before
T8b949e// triggering the liveness timeout. Under UnconfinedTestDispatcher the launch runs
T8b949e// eagerly to its first suspension (awaiting SharedFlow emission).
Tff7b72val Te6edf3errors Tff7b72= Te6edf3mutableListOfTff7b72<Tffa657StringTff7b72>Tb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3collectJob Tff7b72= Te6edf3backgroundScopeTb4b4b4.Te6edf3launch Tb4b4b4{ Te6edf3serviceTb4b4b4.Te6edf3connectionErrorTb4b4b4.Te6edf3collect Tb4b4b4{ Te6edf3errorsTb4b4b4.Te6edf3addTb4b4b4(Tffa657itTb4b4b4) Tb4b4b4} Tb4b4b4}

Te6edf3clock Tff7b72= T79c0ff6T79c0ff5Te6edf3_000L
Te6edf3serviceTb4b4b4.Te6edf3checkLivenessTb4b4b4(Tb4b4b4)
T8b949e// The restart completes inline under UnconfinedTestDispatcher; runCurrent/advanceTimeBy
T8b949e// are belt-and-suspenders (mirrors the sibling liveness tests' pattern).
Te6edf3testDispatcherTb4b4b4.Te6edf3schedulerTb4b4b4.Te6edf3runCurrentTb4b4b4(Tb4b4b4)
Te6edf3advanceTimeByTb4b4b4(T79c0ff1Te6edf3_000LTb4b4b4)

Te6edf3collectJobTb4b4b4.Te6edf3cancelTb4b4b4(Tb4b4b4)

Te6edf3assertTrueTb4b4b4(
Te6edf3errorsTb4b4b4.Te6edf3isEmptyTb4b4b4(Tb4b4b4)Tb4b4b4,
Ta5d6ff"Ta5d6ffAutomatic BLE liveness recovery must not emit user-facing connection error (got: Tffd700$Te6edf3errorsTa5d6ff)Ta5d6ff"Tb4b4b4,
Tb4b4b4)
Tb4b4b4} Tff7b72finally Tb4b4b4{
Te6edf3serviceTb4b4b4.Te6edf3disconnectTb4b4b4(Tb4b4b4)
Te6edf3advanceTimeByTb4b4b4(T79c0ff1Te6edf3_000LTb4b4b4)
Tb4b4b4}
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`BLE liveness restart does not send polite disconnect into zombie transport`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTestTb4b4b4(Te6edf3testDispatcherTb4b4b4) Tb4b4b4{
Te6edf3clock Tff7b72= T79c0ff0L
Tff7b72val Te6edf3service Tff7b72= Te6edf3createConnectedServiceTb4b4b4(Ta5d6ff"Ta5d6ffxAA:BB:CC:DD:EE:FFTa5d6ff"Tb4b4b4)
Tff7b72try Tb4b4b4{
Tff7b72val Te6edf3oldTransport Tff7b72= Te6edf3createdTransportsTb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4)

Te6edf3oldTransportTb4b4b4.Te6edf3sentDataTb4b4b4.Te6edf3clearTb4b4b4(Tb4b4b4)

Te6edf3clock Tff7b72= T79c0ff6T79c0ff5Te6edf3_000L
Te6edf3serviceTb4b4b4.Te6edf3checkLivenessTb4b4b4(Tb4b4b4)
Te6edf3testDispatcherTb4b4b4.Te6edf3schedulerTb4b4b4.Te6edf3runCurrentTb4b4b4(Tb4b4b4)
Te6edf3advanceTimeByTb4b4b4(T79c0ff1Te6edf3_000LTb4b4b4)

Te6edf3assertTrueTb4b4b4(
Te6edf3oldTransportTb4b4b4.Te6edf3sentDataTb4b4b4.Te6edf3isEmptyTb4b4b4(Tb4b4b4)Tb4b4b4,
Ta5d6ff"Ta5d6ffPolite disconnect frame must NOT be sent into zombie transport during liveness restartTa5d6ff"Tb4b4b4,
Tb4b4b4)
Tb4b4b4} Tff7b72finally Tb4b4b4{
Te6edf3serviceTb4b4b4.Te6edf3disconnectTb4b4b4(Tb4b4b4)
Te6edf3advanceTimeByTb4b4b4(T79c0ff1Te6edf3_000LTb4b4b4)
Tb4b4b4}
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`BLE repeated liveness checks do not stack restarts`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTestTb4b4b4(Te6edf3testDispatcherTb4b4b4) Tb4b4b4{
Te6edf3clock Tff7b72= T79c0ff0L
Tff7b72val Te6edf3service Tff7b72= Te6edf3createConnectedServiceTb4b4b4(Ta5d6ff"Ta5d6ffxAA:BB:CC:DD:EE:FFTa5d6ff"Tb4b4b4)

Tff7b72try Tb4b4b4{
Te6edf3clock Tff7b72= T79c0ff6T79c0ff5Te6edf3_000L
Te6edf3serviceTb4b4b4.Te6edf3checkLivenessTb4b4b4(Tb4b4b4)
Te6edf3testDispatcherTb4b4b4.Te6edf3schedulerTb4b4b4.Te6edf3runCurrentTb4b4b4(Tb4b4b4)
Te6edf3advanceTimeByTb4b4b4(T79c0ff1Te6edf3_000LTb4b4b4)

Te6edf3clock Tff7b72= T79c0ff6T79c0ff6Te6edf3_000L
Te6edf3serviceTb4b4b4.Te6edf3checkLivenessTb4b4b4(Tb4b4b4)
Te6edf3testDispatcherTb4b4b4.Te6edf3schedulerTb4b4b4.Te6edf3runCurrentTb4b4b4(Tb4b4b4)
Te6edf3advanceTimeByTb4b4b4(T79c0ff1Te6edf3_000LTb4b4b4)

Tff7b72val Te6edf3firstTransportCloses Tff7b72= Te6edf3createdTransportsTb4b4b4.Te6edf3firstOrNullTb4b4b4(Tb4b4b4)Tff7b72?.Te6edf3closeCount Tff7b72?: T79c0ff0
Te6edf3assertEqualsTb4b4b4(T79c0ff1Tb4b4b4, Te6edf3firstTransportClosesTb4b4b4, Ta5d6ff"Ta5d6ffFirst transport should be closed exactly once (no stacking)Ta5d6ff"Tb4b4b4)
Tb4b4b4} Tff7b72finally Tb4b4b4{
Te6edf3serviceTb4b4b4.Te6edf3disconnectTb4b4b4(Tb4b4b4)
Te6edf3advanceTimeByTb4b4b4(T79c0ff1Te6edf3_000LTb4b4b4)
Tb4b4b4}
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`BLE in-flight liveness restart prevents overlapping restart via isRestarting`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTestTb4b4b4(Te6edf3testDispatcherTb4b4b4) Tb4b4b4{
T8b949e// Deterministic in-flight overlap: a GatedFakeRadioTransport holds the first restart
T8b949e// genuinely suspended inside stopTransportLocked β†’ close() (awaiting closeGate). This
T8b949e// removes reliance on UnconfinedTestDispatcher scheduling so the overlap window is real.
T8b949e//
T8b949e// The first checkLiveness() flips state to DeviceSleep and CAS-sets isRestarting before
T8b949e// launching the restart coroutine, which then suspends in close(). The second
T8b949e// checkLiveness() is issued while that restart is still suspended and must NOT begin
T8b949e// another close/create cycle.
Tff7b72val Te6edf3gatedTransports Tff7b72= Te6edf3mutableListOfTff7b72<Te6edf3GatedFakeRadioTransportTff7b72>Tb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3closeGate Tff7b72= Te6edf3CompletableDeferredTff7b72<Tffa657UnitTff7b72>Tb4b4b4(Tb4b4b4)
T8b949e// Publish the gate to activeCloseGate so tearDown can release it even if an assertion below
T8b949e// throws before we reach the try/finally β€” otherwise disconnect() would hang on close().
Te6edf3activeCloseGate Tff7b72= Te6edf3closeGate
Tff7b72val Te6edf3transportProviderTb4b4b4: Tb4b4b4(Tb4b4b4) Tff7b72-Tff7b72> Te6edf3RadioTransport Tff7b72= Tb4b4b4{
Te6edf3GatedFakeRadioTransportTb4b4b4(Te6edf3closeGateTb4b4b4)Tb4b4b4.Te6edf3also Tb4b4b4{ Te6edf3gatedTransportsTb4b4b4.Te6edf3addTb4b4b4(Tffa657itTb4b4b4) Tb4b4b4}
Tb4b4b4}

Te6edf3clock Tff7b72= T79c0ff0L
Tff7b72val Te6edf3service Tff7b72= Te6edf3createConnectedServiceTb4b4b4(Ta5d6ff"Ta5d6ffxAA:BB:CC:DD:EE:FFTa5d6ff"Tb4b4b4, Te6edf3transportProviderTb4b4b4)
Tff7b72try Tb4b4b4{
Te6edf3assertEqualsTb4b4b4(T79c0ff1Tb4b4b4, Te6edf3gatedTransportsTb4b4b4.Te6edf3sizeTb4b4b4, Ta5d6ff"Ta5d6ffInitial connect should create one transportTa5d6ff"Tb4b4b4)
Tff7b72val Te6edf3initialTransport Tff7b72= Te6edf3gatedTransportsTb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4)

T8b949e// Past the 60s threshold β†’ first checkLiveness triggers a restart whose close() suspends
T8b949e// on closeGate. Under UnconfinedTestDispatcher the launched restart runs eagerly up to the
T8b949e// suspension point, so by the time checkLiveness() returns the restart is in-flight.
Te6edf3clock Tff7b72= T79c0ff6T79c0ff5Te6edf3_000L
Te6edf3serviceTb4b4b4.Te6edf3checkLivenessTb4b4b4(Tb4b4b4)

T8b949e// Issue a second checkLiveness() while the first restart is still suspended in close().
T8b949e// Do NOT advance time here β€” the overlap must happen with the first restart in-flight.
Te6edf3clock Tff7b72= T79c0ff6T79c0ff5Te6edf3_001L
Te6edf3serviceTb4b4b4.Te6edf3checkLivenessTb4b4b4(Tb4b4b4)

T8b949e// Assertions below run while the restart is held suspended on closeGate. They MUST be
T8b949e// wrapped in try/finally so closeGate is completed even if one of them fails; otherwise
T8b949e// tearDown's runBlocking { disconnect() } would hang forever on the gated close().
Tff7b72try Tb4b4b4{
T8b949e// While the first restart is suspended: exactly one transport created so far, and close()
T8b949e// was entered exactly once and has NOT completed. The second check started no new cycle.
Te6edf3assertEqualsTb4b4b4(
T79c0ff1Tb4b4b4,
Te6edf3gatedTransportsTb4b4b4.Te6edf3sizeTb4b4b4,
Ta5d6ff"Ta5d6ffSecond check must not create a transport while the first restart is in-flightTa5d6ff"Tb4b4b4,
Tb4b4b4)
Te6edf3assertTrueTb4b4b4(
Te6edf3initialTransportTb4b4b4.Te6edf3closeCalledTb4b4b4,
Ta5d6ff"Ta5d6ffFirst transport close must have been entered by the restartTa5d6ff"Tb4b4b4,
Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(
T79c0ff1Tb4b4b4,
Te6edf3initialTransportTb4b4b4.Te6edf3closeCountTb4b4b4,
Ta5d6ff"Ta5d6ffclose() entered exactly once (no stacking of close calls)Ta5d6ff"Tb4b4b4,
Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(
T79c0ff0Tb4b4b4,
Te6edf3initialTransportTb4b4b4.Te6edf3closeCompletedCountTb4b4b4,
Ta5d6ff"Ta5d6ffclose() must still be suspended (restart held in-flight) before releasing the gateTa5d6ff"Tb4b4b4,
Tb4b4b4)
Tb4b4b4} Tff7b72finally Tb4b4b4{
T8b949e// Release the gate unconditionally so the suspended restart can complete. tearDown
T8b949e// also releases activeCloseGate, but completing it here is required for the post-finally
T8b949e// assertions below to observe the resumed restart.
Te6edf3closeGateTb4b4b4.Te6edf3completeTb4b4b4(Tffa657UnitTb4b4b4)
Tb4b4b4}

T8b949e// Release the gate: the suspended restart resumes, completes stopTransportLocked (whose
T8b949e// polite-disconnect delay is 500ms β€” covered by the 1s below), and startTransportLocked
T8b949e// creates the single fresh transport. isRestarting is reset in the finally block.
Te6edf3testDispatcherTb4b4b4.Te6edf3schedulerTb4b4b4.Te6edf3runCurrentTb4b4b4(Tb4b4b4)
Te6edf3advanceTimeByTb4b4b4(T79c0ff1Te6edf3_000LTb4b4b4)

T8b949e// Exactly 2 transports: 1 initial + 1 restart. A stacking bug would produce 3+.
Te6edf3assertEqualsTb4b4b4(
T79c0ff2Tb4b4b4,
Te6edf3gatedTransportsTb4b4b4.Te6edf3sizeTb4b4b4,
Ta5d6ff"Ta5d6ffExactly one fresh transport created after the restart resumes (1 initial + 1 restart)Ta5d6ff"Tb4b4b4,
Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(
T79c0ff1Tb4b4b4,
Te6edf3initialTransportTb4b4b4.Te6edf3closeCountTb4b4b4,
Ta5d6ff"Ta5d6ffFirst transport still closed exactly once after restart completesTa5d6ff"Tb4b4b4,
Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff1Tb4b4b4, Te6edf3initialTransportTb4b4b4.Te6edf3closeCompletedCountTb4b4b4, Ta5d6ff"Ta5d6ffFirst transport close completed exactly onceTa5d6ff"Tb4b4b4)
Tb4b4b4} Tff7b72finally Tb4b4b4{
Te6edf3serviceTb4b4b4.Te6edf3disconnectTb4b4b4(Tb4b4b4)
Te6edf3advanceTimeByTb4b4b4(T79c0ff1Te6edf3_000LTb4b4b4)
Tb4b4b4}
Tb4b4b4}

T8b949e// ─── Non-BLE: Liveness does not mutate state ───────────────────────────────────────────────

Tf0883e@Test
Tff7b72fun Td2a8ff`non-BLE transport liveness timeout does not close transport or change state`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTestTb4b4b4(Te6edf3testDispatcherTb4b4b4) Tb4b4b4{
Te6edf3clock Tff7b72= T79c0ff0L
Tff7b72val Te6edf3service Tff7b72= Te6edf3createConnectedServiceTb4b4b4(Ta5d6ff"Ta5d6fft192.168.1.100Ta5d6ff"Tb4b4b4)
Tff7b72try Tb4b4b4{
Tff7b72val Te6edf3stateBefore Tff7b72= Te6edf3serviceTb4b4b4.Te6edf3connectionStateTb4b4b4.Te6edf3value

Te6edf3clock Tff7b72= T79c0ff6T79c0ff5Te6edf3_000L
Te6edf3serviceTb4b4b4.Te6edf3checkLivenessTb4b4b4(Tb4b4b4)
Te6edf3testDispatcherTb4b4b4.Te6edf3schedulerTb4b4b4.Te6edf3runCurrentTb4b4b4(Tb4b4b4)
Te6edf3advanceTimeByTb4b4b4(T79c0ff1Te6edf3_000LTb4b4b4)

Te6edf3assertEqualsTb4b4b4(Te6edf3stateBeforeTb4b4b4, Te6edf3serviceTb4b4b4.Te6edf3connectionStateTb4b4b4.Te6edf3valueTb4b4b4, Ta5d6ff"Ta5d6ffNon-BLE state must not changeTa5d6ff"Tb4b4b4)
Te6edf3assertFalseTb4b4b4(Te6edf3createdTransportsTb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3closeCalledTb4b4b4, Ta5d6ff"Ta5d6ffNon-BLE transport must NOT be closedTa5d6ff"Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff1Tb4b4b4, Te6edf3createdTransportsTb4b4b4.Te6edf3sizeTb4b4b4, Ta5d6ff"Ta5d6ffNo restart should occur for non-BLE transportTa5d6ff"Tb4b4b4)
Tb4b4b4} Tff7b72finally Tb4b4b4{
Te6edf3serviceTb4b4b4.Te6edf3disconnectTb4b4b4(Tb4b4b4)
Te6edf3advanceTimeByTb4b4b4(T79c0ff1Te6edf3_000LTb4b4b4)
Tb4b4b4}
Tb4b4b4}

T8b949e// ─── handleFromRadio resets the liveness timer ──────────────────────────────────────────────

Tf0883e@Test
Tff7b72fun Td2a8ff`inbound data resets liveness timer so timeout does not fire`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTestTb4b4b4(Te6edf3testDispatcherTb4b4b4) Tb4b4b4{
Te6edf3clock Tff7b72= T79c0ff0L
Tff7b72val Te6edf3service Tff7b72= Te6edf3createConnectedServiceTb4b4b4(Ta5d6ff"Ta5d6ffxAA:BB:CC:DD:EE:FFTa5d6ff"Tb4b4b4)

Tff7b72try Tb4b4b4{
T8b949e// Advance 30s, then receive data (resets lastDataReceivedMillis to clock=30s)
Te6edf3clock Tff7b72= T79c0ff3T79c0ff0Te6edf3_000L
Te6edf3serviceTb4b4b4.Te6edf3handleFromRadioTb4b4b4(Te6edf3byteArrayOfTb4b4b4(T79c0ff1Tb4b4b4, T79c0ff2Tb4b4b4, T79c0ff3Tb4b4b4)Tb4b4b4)

T8b949e// 30s since last data β†’ within 60s threshold β†’ should NOT fire
Te6edf3clock Tff7b72= T79c0ff6T79c0ff0Te6edf3_000L
Te6edf3serviceTb4b4b4.Te6edf3checkLivenessTb4b4b4(Tb4b4b4)
Te6edf3assertFalseTb4b4b4(
Te6edf3createdTransportsTb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3closeCalledTb4b4b4,
Ta5d6ff"Ta5d6ffLiveness must not fire when silence is within threshold after inbound dataTa5d6ff"Tb4b4b4,
Tb4b4b4)

T8b949e// 66s since last data (at t=30s) β†’ past 60s threshold β†’ should fire
Te6edf3clock Tff7b72= T79c0ff9T79c0ff6Te6edf3_000L
Te6edf3serviceTb4b4b4.Te6edf3checkLivenessTb4b4b4(Tb4b4b4)
Te6edf3testDispatcherTb4b4b4.Te6edf3schedulerTb4b4b4.Te6edf3runCurrentTb4b4b4(Tb4b4b4)
Te6edf3advanceTimeByTb4b4b4(T79c0ff1Te6edf3_000LTb4b4b4)
Te6edf3assertTrueTb4b4b4(
Te6edf3createdTransportsTb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3closeCalledTb4b4b4,
Ta5d6ff"Ta5d6ffLiveness should fire after silence exceeds threshold since last inbound dataTa5d6ff"Tb4b4b4,
Tb4b4b4)
Tb4b4b4} Tff7b72finally Tb4b4b4{
Te6edf3serviceTb4b4b4.Te6edf3disconnectTb4b4b4(Tb4b4b4)
Te6edf3advanceTimeByTb4b4b4(T79c0ff1Te6edf3_000LTb4b4b4)
Tb4b4b4}
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`BLE liveness does not fire when connection state is not Connected`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTestTb4b4b4(Te6edf3testDispatcherTb4b4b4) Tb4b4b4{
Te6edf3clock Tff7b72= T79c0ff0L
Tff7b72val Te6edf3service Tff7b72= Te6edf3createConnectedServiceTb4b4b4(Ta5d6ff"Ta5d6ffxAA:BB:CC:DD:EE:FFTa5d6ff"Tb4b4b4)

Tff7b72try Tb4b4b4{
Te6edf3serviceTb4b4b4.Te6edf3onDisconnectTb4b4b4(Te6edf3isPermanent Tff7b72= Tff7b72trueTb4b4b4)
Te6edf3assertFalseTb4b4b4(Te6edf3serviceTb4b4b4.Te6edf3connectionStateTb4b4b4.Te6edf3value Tff7b72=Tff7b72= Te6edf3ConnectionStateTb4b4b4.Te6edf3ConnectedTb4b4b4)

Te6edf3clock Tff7b72= T79c0ff6T79c0ff5Te6edf3_000L
Te6edf3serviceTb4b4b4.Te6edf3checkLivenessTb4b4b4(Tb4b4b4)
Te6edf3testDispatcherTb4b4b4.Te6edf3schedulerTb4b4b4.Te6edf3runCurrentTb4b4b4(Tb4b4b4)
Te6edf3advanceTimeByTb4b4b4(T79c0ff1Te6edf3_000LTb4b4b4)
Te6edf3assertFalseTb4b4b4(Te6edf3createdTransportsTb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3closeCalledTb4b4b4, Ta5d6ff"Ta5d6ffLiveness must not fire when not ConnectedTa5d6ff"Tb4b4b4)
Tb4b4b4} Tff7b72finally Tb4b4b4{
Te6edf3serviceTb4b4b4.Te6edf3disconnectTb4b4b4(Tb4b4b4)
Te6edf3advanceTimeByTb4b4b4(T79c0ff1Te6edf3_000LTb4b4b4)
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}

Served by rngit 1.5.0 - Generated in 0.11s